Skip to content

Add opt-in managed Python environment for FUSE mounting on macOS - #2528

Open
AlBannaTechno wants to merge 1 commit into
borgbase:masterfrom
AlBannaTechno:feature/managed-mount-venv
Open

Add opt-in managed Python environment for FUSE mounting on macOS#2528
AlBannaTechno wants to merge 1 commit into
borgbase:masterfrom
AlBannaTechno:feature/managed-mount-venv

Conversation

@AlBannaTechno

Copy link
Copy Markdown

Problem

On macOS, the Mount feature is broken for a large class of users, for reasons outside their control:

  1. The bundled Borg binary has no FUSE support. Notarization and hardened-runtime restrictions prevent the .app bundle from dynamically loading external FUSE libraries (macFUSE/FUSE-T) at runtime, so shipping a mount-capable Borg inside the app is not feasible. This is a known, long-standing limitation.
  2. The documented workaround is fragile. The Homebrew borgbackup/tap/borgbackup-fuse formula's build check requires /usr/local/include/fuse/fuse.h to exist and not be a symlink. On systems using FUSE-T — the modern, kext-less FUSE implementation — that path is a symlink to FUSE-T's headers, so the install is rejected outright even though FUSE-T is an API-compatible drop-in for macFUSE and its headers are perfectly valid.
  3. The remaining alternative is macFUSE's kernel extension, which requires a Recovery Mode security downgrade, kext approval, and a restart — a poor and error-prone experience on modern macOS, and unnecessary when FUSE-T already works without any of it.

The net effect: a user with a healthy FUSE-T installation has no reasonable path to a working Mount button.

Solution

Building Borg from source via pip install "borgbackup[llfuse]" inside an isolated virtual environment produces a mount-capable Borg on the first try — the compiler links against whatever FUSE headers are present at build time, without caring whether they belong to macFUSE or FUSE-T. This was validated manually on Apple Silicon with FUSE-T before implementation: the venv-built Borg mounts and browses real repositories with no symlink workarounds, no kext, no Recovery Mode.

This PR wraps that recipe in a fully opt-in feature:

  • A new macOS-only setting, "Use managed Python environment for mounting" (off by default), in a new Mounting group on the Settings view, with a Prepare Environment button and live status output.
  • A new vorta.borg.fuse_venv module that builds the environment at <app support>/Vorta/borg-fuse-venv/ as a background job on the existing JobsManager infrastructure (own site key, so it never queues behind repository jobs).
  • A targeted override of BorgMountJob.prepare_bin(): when the setting is enabled and the environment has passed validation, the mount command uses the venv's Borg binary by absolute path. Nothing else changes.

User flow

  1. Enable the checkbox in Settings, click Prepare Environment.
  2. The job creates the venv, upgrades packaging tools, compiles Borg with llfuse, then verifies the result (borg --version runs and llfuse imports) before writing a readiness marker. Progress streams line-by-line into the UI.
  3. Mount/Unmount on the Archives tab work exactly as before — only the binary underneath differs.

Robustness

  • No silent half-built state. The readiness marker is written only after validation; it is removed before any rebuild. A failed or cancelled build can never be treated as ready, and Mount falls back to the default binary resolution if the environment is missing or invalid.
  • Actionable errors instead of pip tracebacks. Missing pkg-config is detected before the build starts, and known failure signatures (missing pkg-config, missing Xcode Command Line Tools) are translated into messages with the exact command to run. The feature never installs anything system-wide itself.
  • Correct interpreter discovery. sys.executable is the Vorta binary when running from the bundled .app, so the venv is created with a discovered system python3 instead.
  • Explicit rebuilds. Re-running setup uses venv --clear and clearly reports that a rebuild is in progress.
  • No FUSE-implementation detection. The code deliberately contains no macFUSE-vs-FUSE-T logic; binding to whatever is installed at build time is the entire point, and any detection would reintroduce the brittleness this PR removes.

Scope and compatibility

  • Only borg mount is affected. Create, prune, check, umount, scheduling, etc. keep the unchanged BorgJob.prepare_bin() resolution — a dedicated test asserts this.
  • Zero behavior change for existing users. The setting is off by default, created through the existing get_or_create settings pass (no schema migration), and the entire feature is guarded to macOS.
  • UI thread stays responsive. The build runs on the existing background job infrastructure with cancellation support, mirroring BorgJob.

Changes

File Change
src/vorta/borg/fuse_venv.py New: venv location, readiness/validation, background setup job
src/vorta/borg/mount.py BorgMountJob.prepare_bin() override (mount-only binary selection)
src/vorta/store/settings.py New darwin-only checkbox setting in a Mounting group
src/vorta/views/misc_tab.py Prepare/Rebuild button and status line under the new checkbox
tests/unit/test_fuse_venv.py New: setting declaration, binary selection, fallback, non-mount isolation

Testing

  • 5 new unit tests; full unit suite passes (230 passed, 7 skipped).
  • Underlying recipe validated manually end-to-end on macOS (Apple Silicon, FUSE-T): venv build, real repository mount, and browsing.

Known limitation

The mount command syntax is derived from the system Borg's detected version (borg_compat), while pip installs the latest stable 1.x. A user whose system Borg is a 2.x beta would get v2-style flags passed to the venv's 1.x binary. Considered acceptable for now given 2.x is pre-release; pinning or per-binary version detection can follow if needed.

Vorta's bundled Borg binary cannot ship FUSE support on macOS due to
notarization and hardened-runtime restrictions, so the Mount feature fails
for many users, and the documented Homebrew fuse-tap workaround breaks when
FUSE-T (rather than macFUSE) provides the FUSE headers. This adds an
alternative, fully opt-in mount mode that builds Borg from source with
llfuse inside a private virtual environment, linking against whichever FUSE
implementation is already installed.

- New setting "Use managed Python environment for mounting" (macOS only,
  off by default) in a new Mounting group on the Settings view (opened via
  the "Settings / About" button; MiscTab in code), with a Prepare/Rebuild
  Environment button and live status output.
- New vorta.borg.fuse_venv module: builds the venv at
  <app support>/Vorta/borg-fuse-venv/ via a JobsManager background job,
  pre-checks for pkg-config and surfaces known build failures (missing
  pkg-config, missing Xcode CLT) as actionable messages, and only marks the
  environment ready after validating borg --version and the llfuse import.
- BorgMountJob.prepare_bin() now prefers the venv's borg binary when the
  setting is enabled and the environment is validated; all other Borg
  commands (create, prune, check, umount, ...) are untouched.
- Unit tests covering setting declaration, binary selection, fallback
  behavior, and that non-mount jobs remain unaffected.

Users who never enable the setting see zero behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant